#!/usr/bin/env ruby

#Universidad del Valle de Guatemala
#Sistemas Operativos Avanzados
#David Ytzen Hsieh Lo #08225

require 'socket'
require 'thread'
require 'monitor'
require 'rexml/document'
require 'logger'
require 'base64'
require 'net/http'
require 'timeout'

include REXML
include Socket::Constants


$myHostname = "david"

#Load config xml file
file = File.new("config.xml")
doc = Document.new(file)

#Parse config xml tree
root = doc.root
$baseDirPath = root.elements["baseDir"].text  
$baseDirPath.gsub!(/[\n]+/, ""); #replace \n with ''

#Load hosts xml file
hostsFile = File.new("hosts.xml")
hostsDoc = Document.new(hostsFile)

#Parse hosts xml tree
hostsRoot = hostsDoc.root
$hostsIP = {
  "david"  => hostsRoot.elements["david"].attributes["ip"].to_s,
  "karen"  => hostsRoot.elements["karen"].attributes["ip"].to_s,
  "kmels"  => hostsRoot.elements["kmels"].attributes["ip"].to_s,
  "asu"    => hostsRoot.elements["asu"].attributes["ip"].to_s,
  "wiikds" => hostsRoot.elements["kmels"].attributes["ip"].to_s,
  "jdiegod"=> hostsRoot.elements["jdiegod"].attributes["ip"].to_s
}

$hostsPort = {
  "david"  => hostsRoot.elements["david"].attributes["port"].to_s,
  "karen"  => hostsRoot.elements["karen"].attributes["port"].to_s,
  "kmels"  => hostsRoot.elements["kmels"].attributes["port"].to_s,
  "asu"    => hostsRoot.elements["asu"].attributes["port"].to_s,
  "wiikds" => hostsRoot.elements["kmels"].attributes["port"].to_s,
  "jdiegod"=> hostsRoot.elements["jdiegod"].attributes["port"].to_s
}

$hostsPriority = {
  "david"  => hostsRoot.elements["david"].attributes["priority"].to_s,
  "karen"  => hostsRoot.elements["karen"].attributes["priority"].to_s,
  "kmels"  => hostsRoot.elements["kmels"].attributes["priority"].to_s,
  "asu"    => hostsRoot.elements["asu"].attributes["priority"].to_s,
  "wiikds" => hostsRoot.elements["kmels"].attributes["priority"].to_s,
  "jdiegod"=> hostsRoot.elements["jdiegod"].attributes["priority"].to_s
}


#Add / if missing
if ($baseDirPath[$baseDirPath.length-1] != 47)
  $baseDirPath += "/"
end
#REMOVE
puts $baseDirPath

#Create log
$log = Logger.new("./server.log")
$log.level = Logger::DEBUG

$log.info "\n::::::::::SERVER STARTED::::::::::\n"

#Create Cache hash
$cache = Hash.new
$cacheQueue = Array.new

#Load passwords

$passwords = Array.new

File.open($baseDirPath + "passwd", 'r') { |f|
  
  while (line = f.gets)
    $passwords.push(line)
  end
}

#Sets the new coordinator
def setCoordinator
  $hostsPriority.sort{|a,b| a[1]<=>b[1]}.each do |node|
    puts "Priority: #{node[0]}"
    if node[0] == $myHostname
      $coordinatorHostname = node[0]
      $coordinatorIP = $hostsIP[node[0]]
      $coordinatorPort = $hostsPort[node[0]]
      return true
    else
      if (isOnline(node[0]))
        $coordinatorHostname = node[0]
        $coordinatorIP = $hostsIP[node[0]]
        $coordinatorPort = $hostsPort[node[0]]
        return true
      end      
    end
  end
  return false
end

#Checks if a node is online
def isOnline(hostname)
  puts "Pinging #{hostname}"
  if($hostsIP.has_key?(hostname))

    wSocket = Socket.new( AF_INET, SOCK_STREAM, 0 )
    sockaddr = Socket.pack_sockaddr_in($hostsPort[hostname].to_i, $hostsIP[hostname])
    puts "Before timeout"
    begin
      status = Timeout::timeout(1) {
        puts "Begin timeout count"
        begin
          wSocket.connect(sockaddr)
        rescue
          puts "#{hostname} is offline: Refuses connection"
          return false
        end
        puts "#{hostname} is online"
        wSocket.close        
    }
    rescue Timeout::Error
      puts "#{hostname} is offline: Timeout"
      return false
    end
    puts "Status: #{status}"
    return true
  end
  return false
end


def createUser(user, password)
  puts "Start of createUser function"
  $passwords.each { |line|
    if line =~ /^#{user}:.*$/
      puts "User already exists"
      return false
    end
  }
  
  $passwords.push("#{user}:#{password}")
  puts "User added to array"
  
  updatePasswordsFile

  return true

end

def editUser(user, password, newPassword)
 puts "Start of editUser function"
 edited = false
 newPasswords = Array.new
 $passwords.each { |line|
   newLine = line
   if line =~ /^#{user}:#{password}$/
     newLine = "#{user}:#{newPassword}"
  
     edited = true
   end
     newPasswords.push(newLine)
   
 }
 $passwords.replace(newPasswords)
 updatePasswordsFile
 
 return edited
end

def deleteUser(user, password)
  puts "Start of deleteUser function"
  deleted = false
  newPasswords = Array.new

  $passwords.each { |line|
    newLine = line
    if line =~ /^#{user}:#{password}$/
      deleted = true
    else     
      newPasswords.push(newLine)
    end
    
    
  }
  $passwords.replace(newPasswords)
  updatePasswordsFile
  return deleted
end

def updatePasswordsFile
  File.open("./passwd", "w") do |file|
    $passwords.each { |line|
      file.puts line
    }

  end

end

setCoordinator()

puts "The coordinator is #{$coordinatorHostname}"

class HTTPServer

  currentFileContent = Array.new

  def initialize(socket)
    #loop do
    @socket = socket
    @socketRequest = Array.new
    request = @socket.readline         
    @socketRequest.push(request);
    puts request
    @clientIP = Socket.unpack_sockaddr_in(socket.getpeername)[1]
    puts "CLIENTIP: #{@clientIP}"
    #Log request
    request.gsub!(/[\n]+/, "");
    $log.info "----------------------------------------------\n"
    $log.info request
    
    if request =~ /GET .* HTTP.*/
      fileName = request.gsub(/GET /, '').gsub(/ HTTP.*/, '')
    end

    if(File.directory?(fileName))
      fileName = "index.html"
    end


    #Get current time
    cTime = Time.now.to_a
    fCTime = Time.utc(*cTime)    
    
    
    #Log host
    #Parse the head info
    host = @socket.readline
    @socketRequest.push(host)
    host.chomp
    $log.info host
    
    #Remove port from hostname
    host = host.split(':')[1].strip
    @containsAuth = false
    while((head= @socket.readline).length > 2 )
      @socketRequest.push(head)
      #Look for authorization in head
      if(head =~ /Authorization(.)*/)
        @authentication = head
        @containsAuth = true
        puts "Contains auth"
        
        
      end
    end
   
    puts "HERE"
    puts @socketRequest

    if(File.extname(fileName) == ".htmls") 
  
  
  
      if($coordinatorHostname == $myHostname)
        
        if(@containsAuth)
      
        authorized = false
        codedPass = @authentication.split[2]
        puts codedPass
        decodedPass = Base64::decode64(codedPass)
        puts decodedPass
          
        #Check password existence in file
        $passwords.each { |pass|         
          if(pass.strip == decodedPass.strip)
            puts "IS #{pass.strip} == #{decodedPass.strip}?"
            authorized = true

          end
        }

        else
          @socket.puts "HTTP/1.1 403 FORBIDDEN\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          
        end

        
        
      else
        result = resolveHostname($coordinatorHostname, "authenticate/")
        if(result =~ /HTTP\/1.1 200 OK.*/)
          authorized = true
        else
          authorized = false
        end
      end

    end
    
      
    
    
    validGET = request.match(/GET .* HTTP\/1\.1/)
    puts validGET
    
    createUserRegex = /createuser\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/*/
    editUserRegex = /edituser\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/*/
    deleteUserRegex = /deleteuser\/([a-zA-Z0-9]+)\/([a-zA-Z0-9]+)\/*/
    authenticateUserRegex = /authenticate\/*/
    
    
    
    
    #Check regex for user creation, modification or deletion
    
    

    case fileName 
      
    when authenticateUserRegex
      
      puts "Did match authenticate regex"
      
      if($coordinatorHostname == $myHostname)
        puts "IM THE BOSS AND SOMEONE NEEDS AUTH "
        puts @clientIP
        if($hostsIP.has_value?(@clientIP)) 
          
          puts "Auth request from a node"
          puts "Authentication #{@authentication}"
          found = false
          codedPass = @authentication.split[2]
          puts codedPass
          decodedPass = Base64::decode64(codedPass)
          puts decodedPass
          
          #Check password existence in file
          $passwords.each { |pass|         
            if(pass.strip == decodedPass.strip)
              
              found = true
              
              @socket.puts "HTTP/1.1 200 OK\n"
              @socket.puts "Connection: close\n"
              @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
              puts "PSW FOUND"
              return 
            end
          }
          puts "CHECKED PASW"
          unless found
            @socket.puts "HTTP/1.1 403 FORBIDDEN\n"
            @socket.puts "Connection: close\n"
            @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          end
        else
          
          @socket.puts "HTTP/1.1 403 FORBIDDEN\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          
        end
      else
        @socket.puts "HTTP/1.1 403 FORBIDDEN\n"
        @socket.puts "Connection: close\n"
        @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
      end
      
    when createUserRegex
      
      puts "Did match createuser regex"
      if($coordinatorHostname == $myHostname || @clientIP == $coordinatorIP)  
        
        match = createUserRegex.match fileName
        
        if (createUser(match[1], match[2]))
          
          
          #If Im the coordinator, then send message to all nodes
          if ($coordinatorHostname == $myHostname) 
            $hostsIP.each_key { |hostname|
              if(isOnline(hostname))
                resolveHostname(hostname, fileName)
              end 
            }
          end
          
          #Respond
          @socket.puts "HTTP/1.1 201 CREATED\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          @socket.puts "\n"
          @socket.puts "User successfuly created"
          
        else
          @socket.puts "HTTP/1.1 409 CONFLICT\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          @socket.puts "\n"
          @socket.puts "User already exists"
        end
        @socket.close
      else
        if(isOnline($coodinatorHostname))
          resolveHostname($coordinatorHostname, fileName)
        else
          setCoordinator()
        end
      end
      
      
      
    when editUserRegex
      puts "Did match edituser regex"
      
      if($coordinatorHostname == $myHostname || @clientIP == $coordinatorIP)  
        
        match = editUserRegex.match fileName
        
        if (editUser(match[1], match[2], match[3]))
          
          
          #If Im the coordinator, then send message to all nodes
          if ($coordinatorHostname == $myHostname) 
            $hostsIP.each_key { |hostname|
              if(isOnline(hostname))
                resolveHostname(hostname, fileName)
              end 
            }
          end
        
          #Respond
          @socket.puts "HTTP/1.1 200 OK\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          @socket.puts "\n"
          @socket.puts "User successfuly modified"
          
          
        else
        
          @socket.puts "HTTP/1.1 404 NOT FOUND\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          @socket.puts "\n"
          @socket.puts "User does not exist"
        end
        
        @socket.close
        
      else
        if(isOnline($coodinatorHostname))
          resolveHostname($coordinatorHostname, fileName)
        else
          setCoordinator()
        end
        
      end
    
    when deleteUserRegex
      puts "Did match deleteuser regex"
      
      if($coordinatorHostname == $myHostname || @clientIP == $coordinatorIP)  
        
        match = deleteUserRegex.match fileName
        
        if (deleteUser(match[1], match[2]))
          
          
          #If Im the coordinator, then send message to all nodes
          if ($coordinatorHostname == $myHostname) 
            $hostsIP.each_key { |hostname|
              if(isOnline(hostname))
                resolveHostname(hostname, fileName)
              end 
            }
          end
          
          #Respond
          @socket.puts "HTTP/1.1 200 OK\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          @socket.puts "\n"
          @socket.puts "User successfuly deleted"
          
          
        else
          
          @socket.puts "HTTP/1.1 404 NOT FOUND\n"
          @socket.puts "Connection: close\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)\n"
          @socket.puts "\n"
          @socket.puts "User does not exist"
        end
        
        @socket.close
        
        
      else
        if(isOnline($coodinatorHostname))
          resolveHostname($coordinatorHostname, fileName)
        else
          setCoordinator()
        end
        
      end
      
    else
      
    end
    
    if(fileName =~ createUserRegex || fileName =~ editUserRegex || fileName =~ deleteUserRegex)
      
      
      
    end  
    
    
    
    
    
    unless (fileName =~ createUserRegex || fileName =~ editUserRegex || fileName =~ deleteUserRegex)
      
      
      

      
      
      
      
      
      #If the request is to another node
      resolvedContent = resolveHostname(host, fileName)    
      unless resolvedContent == ""
        
        @socket.puts resolvedContent
      else
        
        unless (validGET)
          puts "Bad request"
          @socket.puts "HTTP/1.1 400 Bad Request\n"
          $log.info "HTTP/1.1 400 Bad Request"
          @socket.close
          next
        end
        
        file = File.basename(fileName)
        
        
        unless ( File.exists?(file) )
          puts "Not found"
          $log.info "HTTP/1.1 404 File Not Found"
          $log.info "Requested file: " + file
          @socket.puts "HTTP/1.1 404 File Not Found\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)"
          @socket.puts "\n"
          sendFile("404.html", @socket)
          @socket.close
          next    
        end
        
        
        unless (authorized)
          @socket.puts "HTTP/1.1 401 Authorization Required\n"
          @socket.puts "Server: IfritServer (Unix) (Debian/Linux)"
          @socket.puts "Date: " + fCTime.inspect +  "\n"
          @socket.puts "WWW-Authenticate: Basic realm='Secure Area'\n"
          @socket.puts "Content-Type: text/html\n"
          @socket.puts "Content-Length: 301 \n"
          
          @socket.puts "\n"
          sendFile("403.html", @socket)
          @socket.close
        end
        
        
        $log.info "HTTP/1.1 200 OK"
        @socket.puts "HTTP/1.1 200 OK\n"
        @socket.puts "Connection: close\n"
        @socket.puts "Server: IfritServer (Unix) (Debian/Linux)"
        @socket.puts "Date:" + fCTime.inspect + "\n"
        
        
        fileType = getFileType(file)
        puts fileType
        @socket.puts "Content-Type: "  + fileType + "\n"
        
        
        $log.info "Requested file: " + file
        
        
        
        fileSize = File.size?(fileName)
        @socket.puts "Content-Length: " + fileSize.to_s() + "\n\n"
        
        $log.info fileSize.to_s() + " bytes transfered"
        
        
        
        #Check if file exists in cache
        if ($cache.has_key?(file)) 
          
          currentFileContent = $cache[file]
          
          if($cacheQueue.length > 4)
            $cacheQueue.delete(file)
            $cacheQueue.compact
          end
          
          $cacheQueue.push(file)
          
          
          $log.info "Read from cache"
          puts "Read from cache"
          
        else #if the file is not in cache
          
        filePath = $baseDirPath + file;
          $log.info "Read from file"
          
          currentFileContent = Array.new
          
          
          
          #Open file and save contents to currentFileContent
          File.open(filePath, 'r') { |f|
            
          while (line = f.gets)
            currentFileContent.push(line)
            
            # @socket.puts line
          end
          }
          
          if($cacheQueue.length > 4)
            #Delete the last used file on cache
            $cache.delete($cacheQueue[0])
            $cacheQueue.delete_at(0)     
            $cacheQueue.compact
          end
          $cacheQueue.push(file)
          $cache[file] = currentFileContent
          
          
        end
        
        #print each line of the currentFileContent Array
        currentFileContent.each { |li| 
          @socket.puts li
        }
        
      end
      
      @socket.close
    end
  end
  
  
  def getFileType(path)
    ext = File.extname(path)
    return "text/html"  if ext == ".html" or ext == ".htm" or ext == ".htmls"
    return "text/plain" if ext == ".txt"
    return "text/css"   if ext == ".css"
    return "Image/jpeg" if ext == ".jpeg" or ext == ".jpg"
    return "Image/gif"  if ext == ".gif"
    return "Image/bmp"  if ext == ".bmp"
    return "Image/png"  if ext == ".png"
    return "text/plain" if ext == ".rb"
    return "text/xml"   if ext == ".xml"
    return "text/xml"   if ext == ".xsl"
    return "text/html"
  end

  def sendFile(path, socket)
    File.open(path, 'r') { |f|
      while line = f.gets
        socket.puts(line)
      end
    }
  end

  def resolveHostname (hostname, path)
    unless (hostname == $myHostname)
      if ($hostsIP.has_key?(hostname))
        
        hostIP = $hostsIP[hostname]
        hostPort = $hostsPort[hostname].to_i
        puts "Target host IP: #{hostIP} Port: #{hostPort}"
        wSocket = Socket.new( AF_INET, SOCK_STREAM, 0 )
        puts "wSocket created"
        sockaddr = Socket.pack_sockaddr_in(hostPort, hostIP)
        #sockaddr = Socket.pack_sockaddr_in(81, "192.168.2.100")
        puts "sockaddr set"
        wSocket.connect(sockaddr)
        puts "wSocket connected"
        @socketRequest.each { |requestLine|
          wSocket.puts requestLine
        }
        #wSocket.write( "GET / HTTP/1.0\r\n\r\n" )
        wSocket.puts "\n"

        results = wSocket.read
        
        puts "After socket read"
        wSocket.close
        
        return results

      end
     
    end
    return ""
  end

end

server = TCPServer.new('0.0.0.0', '81')

puts "----------------------------------------------"
puts "Starting Ifrit server..."
puts "----------------------------------------------"

File.open('Logo.txt', 'r') do |f1|
  while line = f1.gets
    puts line
  end
end

puts "Server running..."
puts "----------------------------------------------"



while socket = server.accept
  Thread.start(socket) do |socket|
    HTTPServer.new(socket)
  end
end




